import re

SOURCE_TEMPLATE = '''{func_def} {{
  {func_impl}
}}
'''

GETVALUE_TEMPLATE = 'getValue<{ftype}>({pydict_name}, {field_name}, {value});'
GETARRAY_TEMPLATE = \
'getArray<{ftype}>({pydict_name}, {field_name}, {value}, {length});'

BIND_TEMPLATE = '''
class Py{class_name} : public {class_name} {{

}};
PYBIND11_MODULE({prefix}, m) {{
//    py::class_<{class_name}, Py{class_name}> {class_name}_(m, "{class_name}", py::module_local());
//{class_name}_
py::class_<TdApi>(m, "TdApi")
.def(py::init<>())
.def("init", &{class_name}::init)
.def("stop", &{class_name}::stop)
.def("sendBatchOrdersReq", &{class_name}::sendBatchOrdersReq)
{bind_impl}
;
}}
'''


class FuncGenerator:
    def __init__(self, prefix, class_name, funcs, struct_def, channel_map,
                 file_func_header, file_func_source, file_bind_source):
        self.prefix = prefix
        self.class_name = class_name
        self.funcs = funcs
        self.struct_def = struct_def
        self.channel_map = channel_map
        self.file_func_header = file_func_header
        self.file_func_source = file_func_source
        self.file_bind_source = file_bind_source

        self.gen_func_names = []  # 生成的主动函数的名字

    # python dict 赋值给 C++ struct
    def generate_struct_assign(self, struct_name, pydict_name, struct_def):
        body_lines = []
        for field_name, field_def in struct_def['fields'].items():
            if field_name.startswith('__'):  # 私有成员
                continue
            if isinstance(field_def, str):
                body_lines.append(
                    GETVALUE_TEMPLATE.format(
                        ftype=field_def,
                        pydict_name=pydict_name,
                        field_name=f'"{field_name}"',
                        value=f'{struct_name}.{field_name}'))
            elif isinstance(field_def, dict):
                if field_def['type'] == 'array':
                    body_lines.append(
                        GETARRAY_TEMPLATE.format(
                            ftype=field_def['ele_type'],
                            pydict_name=pydict_name,
                            field_name=f'"{field_name}"',
                            value=f'{struct_name}.{field_name}',
                            length=field_def['len']))
                elif field_def['type'] == 'union' or field_def[
                        'type'] == 'struct':
                    udict_name = f'{pydict_name}_{field_name}'
                    body_lines.append(
                        f'if ({pydict_name}.contains("{field_name}")) {{')
                    body_lines.append(
                        f'auto {udict_name} = {pydict_name}["{field_name}"].cast<py::dict>();'
                    )
                    body_lines.extend(
                        self.generate_struct_assign(
                            f'{struct_name}.{field_name}', udict_name,
                            field_def))
                    body_lines.append('}')
                else:
                    raise RuntimeError(
                        f'Unknown field {struct_name} {field_name}')
            else:
                raise RuntimeError(f'Unknown field {struct_name} {field_name}')
        return body_lines

    def generate_dict_assign(self, struct_name, pydict_name, struct_def):
        body_lines = []
        for field_name, field_def in struct_def['fields'].items():
            if field_name.startswith('__'):  # 私有成员
                continue
            if isinstance(field_def, str):
                body_lines.append(
                    f'{pydict_name}["{field_name}"] = {struct_name}.{field_name};'
                )
            elif isinstance(field_def, dict):
                if field_def['type'] == 'array':
                    if field_def['ele_type'] == 'char':
                        body_lines.append(
                            f'{pydict_name}["{field_name}"] = std::string({struct_name}.{field_name});'
                        )
                    else:
                        ulist = f'{pydict_name}_{field_name}'
                        body_lines.append(f'auto {ulist} = py::list();')
                        body_lines.append(
                            f'for(const auto &x:{struct_name}.{field_name}){ulist}.append(x);'
                        )
                        body_lines.append(f'{pydict_name}["{field_name}"] = {ulist};')
                elif field_def['type'] == 'union' or field_def[
                        'type'] == 'struct':
                    udict = f'{pydict_name}_{field_name}'
                    body_lines.append(f'py::dict {udict};')
                    body_lines.extend(
                        self.generate_dict_assign(
                            f'{struct_name}.{field_name}', udict,
                             field_def))
                    body_lines.append(f'{pydict_name}["{field_name}"] = {udict};')
                else:
                    raise RuntimeError(
                        f'Unknown field {struct_name} {field_name}')
            else:
                raise RuntimeError(f'Unknown field {struct_name} {field_name}')
        return body_lines

    def generate_function(self, func_name, params, ret_type, fheader, fsource):
        camel_pattern = re.compile(r'(?<!^)(?=[A-Z])')

        if params[0]['type'] != 'OesAsyncApiChannelT*' and params[0]['type'] != 'OesApiSessionInfoT*':
            raise RuntimeError('Function must start with a channel')

        gen_ret_type = [ret_type]
        gen_params = []
        gen_body_lines = []
        gen_call_params = [self.channel_map[params[0]["name"]]]
        if len(params) > 1:
            for p in params[1:]:
                ptype = p['type']
                if ptype.startswith('const '):
                    ptype = ptype[6:]
                if ptype.endswith('T*'):
                    if p['inout'] == 'in':
                        gen_params_str = f'const py::dict& req{p["name"]}'
                        null_obj_name = 'NULLOBJ_' + camel_pattern.sub(
                            '_', ptype[:-2]).upper()
                        gen_body_lines.append(
                            f'{ptype[:-1]} {p["name"]}{{{null_obj_name}}};')
                        gen_body_lines.extend(
                            self.generate_struct_assign(
                                p['name'], f'req{p["name"]}',
                                self.struct_def[ptype[:-1]]))
                        gen_body_lines.append('')
                    gen_call_params.append(f'&{p["name"]}')
                if ptype == 'char*':
                    gen_params_str = f'const std::string& {p["name"]}'
                    gen_call_params.append(f'{p["name"]}.c_str()')
                if ptype in ['int32', 'int64', 'BOOL']:
                    gen_params_str = f'{ptype} {p["name"]}'
                    gen_call_params.append(p['name'])

                if p['inout'] == 'in':
                    gen_params.append(gen_params_str)
                else:
                    gen_ret_type.append(p)

        if len(gen_ret_type) > 1:
            gen_ret_type_str = 'py::tuple'
        else:
            gen_ret_type_str = gen_ret_type[0]

        gen_func_name = func_name.split('_')[-1]
        gen_func_name = gen_func_name[0].lower() + gen_func_name[1:]
        func_def_str = f'{gen_ret_type_str} {gen_func_name}({", ".join(gen_params)})'
        fheader.write(func_def_str + ';\n\n')

        if len(gen_ret_type) > 1:
            for p in gen_ret_type[1:]:
                if p["type"].endswith('T*'):
                    gen_body_lines.append(f'{p["type"][:-1]} {p["name"]};')
                else:
                    raise RuntimeError(f'Not supported type {p["type"]}')

        gen_body_lines.append(
            f'{ret_type} ret = {func_name}({", ".join(gen_call_params)});')

        if len(gen_ret_type) > 1:
            for p in gen_ret_type[1:]:
                gen_body_lines.append(f'py::dict dict_{p["name"]};')
                gen_body_lines.extend(
                    self.generate_dict_assign(p['name'], f'dict_{p["name"]}',
                                              self.struct_def[p['type'][:-1]]))
            ret_extra_params_str = ','.join(f'dict_{p["name"]}'
                                            for p in gen_ret_type[1:])
            gen_body_lines.append(
                f'return py::make_tuple(ret, {ret_extra_params_str});')
        else:
            gen_body_lines.append('return ret;')
        func_def_str = f'{gen_ret_type_str} {self.class_name}::{gen_func_name}({", ".join(gen_params)})'
        func_src_str = SOURCE_TEMPLATE.format(
            func_def=func_def_str, func_impl='\n  '.join(gen_body_lines))
        fsource.write(func_src_str + '\n')

        self.gen_func_names.append(gen_func_name)

    def run(self):
        with open(self.file_func_header, 'w') as fheader, \
             open(self.file_func_source, 'w') as fsource:
            for func_name, func_def in self.funcs.items():
                self.generate_function(func_name, func_def['params'],
                                       func_def['ret_type'], fheader, fsource)

        with open(self.file_bind_source, 'w') as fbind:
            bind_lines = []
            for fname in self.gen_func_names:
                bind_lines.append(
                    f'.def("{fname}", &{self.class_name}::{fname})')
            fbind.write(
                BIND_TEMPLATE.format(prefix=self.prefix,
                                     class_name=self.class_name,
                                     bind_impl='\n'.join(bind_lines)))
